Files
bunny-bot/internal/moon/entityGuard.go
eleksir 0c34b6e781 Global code ovehaul - remove CAS and other, add 15 minute greet check
- Remove CAS, as it no longer works for us
- Remove other checks
- Implement 15 minute greet timeout check
- Fix chat logos
- Ditch thirdparty logger, use stdlib log facility wrapper for logging
2026-10-20 12:20:16 +03:00

131 lines
4.7 KiB
Go

package moon
import (
"bunny-bot/internal/mylog"
"github.com/NicoNex/echotron/v3"
"github.com/davecgh/go-spew/spew"
)
// The Bot API always returns a User/Chat carrying its id, and echotron validates
// nothing: lclient.dispatch (network.go:138) is json.Unmarshal plus a check() that
// reads only the ok/error_code envelope (helpers.go:37). An entity that arrives
// without an id therefore reaches us as a silent zero, with no layer below to
// complain about it. These predicates are the whole of our defence.
//
// They are SILENT. A refusal is a fact, not an incident: reporting happens once, at
// the ingress, where the offending update can be dumped whole. A predicate that logs
// would make one broken update produce one line per entity that happens to meet it.
// usableUser reports whether a user entity is one we can actually address.
//
// The test is against zero and not against zero-or-less on purpose: a chat id is
// legitimately negative for a group or supergroup, so a positivity check would
// refuse every group chat in the project.
func usableUser(u *echotron.User) bool {
return u != nil && u.ID != 0
}
// usableChat reports whether a chat entity is one we can actually address, with the
// same reasoning and the same zero-not-positive rule.
func usableChat(c *echotron.Chat) bool {
return c != nil && c.ID != 0
}
// usableUpdate reports whether an update is worth dispatching at all.
//
// Its branches mirror telegramMsgParser's, in the same order and under the same
// conditions, so a branch added to one and not the other is visible by reading. That
// is not tidiness: the alternative is a validator shaped differently from the
// dispatcher, whose coverage nobody can check by eye.
//
// nil is tolerated only where absence is normal. A message with no author is a
// channel post or a service message, and refusing those would disable chat logging
// for channels. A nil element inside NewChatMembers is different — the array asserts
// a newcomer exists — and so is a nil NewChatMember.User, which asserts who changed.
// A nil OldChatMember.User is tolerated: some transitions have no previous member.
func usableUpdate(u *echotron.Update) bool {
if u == nil {
return false
}
switch {
case u.ChatMember != nil:
return usableChatMember(u.ChatMember)
case u.Message != nil:
return usableMessage(u.Message)
case u.EditedMessage != nil:
return usableMessage(u.EditedMessage)
default:
// Nothing we act on. The dispatcher's spew fallback is where this belongs.
return true
}
}
// usableChatMember checks a membership change.
//
// Its entities are values, not pointers: ChatMemberUpdated.Chat, .From and both
// ChatMember structs can never be nil, so there is no nil test to fall back on and
// the id check is the only thing that can catch a malformed one.
func usableChatMember(cm *echotron.ChatMemberUpdated) bool {
return usableChat(&cm.Chat) &&
usableUser(cm.NewChatMember.User) &&
(cm.OldChatMember.User == nil || usableUser(cm.OldChatMember.User)) &&
usableUser(&cm.From)
}
// usableMessage checks a message, or an edit of one — the two carry the same fields
// and are treated the same way.
//
// Message.Chat is a value: a zero chat id would otherwise reach elog, which builds
// chat_logs/<id>/yyyy/mm/dd.txt from it and would silently create a chat_logs/0
// directory and write every malformed update into it.
func usableMessage(m *echotron.Message) bool {
if !usableChat(&m.Chat) {
return false
}
if m.From != nil && !usableUser(m.From) {
return false
}
if m.LeftChatMember != nil && !usableUser(m.LeftChatMember) {
return false
}
for _, member := range m.NewChatMembers {
if !usableUser(member) {
return false
}
}
// One level deep, and the nested chat only. The nested author is never rendered,
// so a zero there is cosmetic; the level bound exists to keep the work constant.
if m.PinnedMessage != nil && !usableChat(&m.PinnedMessage.Chat) {
return false
}
if m.ReplyToMessage != nil && !usableChat(&m.ReplyToMessage.Chat) {
return false
}
return true
}
// reportBrokenUpdate records an update we are refusing, in the terms it deserves:
// this is not a user being difficult and not a transient API error, it is an
// upstream that broke its own contract.
//
// The whole update is dumped, once. The dump is the point — the condition is
// unreproducible and unqueryable, since there is no id to ask Telegram about — and a
// partial dump of one field out of four would not be evidence of anything.
func reportBrokenUpdate(u *echotron.Update) {
mylog.Error(
"Telegram sent an update with an entity carrying no id, refusing it: nothing in it can be greeted, recorded or banned, and an API that sends one is broken",
)
mylog.Error(spew.Sdump(u))
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */