all repos — honk @ d6cf87a29b3e57372e06d933d3a6758d2e550898

my fork of honk

honk.go (view raw)

  1//
  2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
  3//
  4// Permission to use, copy, modify, and distribute this software for any
  5// purpose with or without fee is hereby granted, provided that the above
  6// copyright notice and this permission notice appear in all copies.
  7//
  8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 15
 16package main
 17
 18import (
 19	"flag"
 20	"fmt"
 21	"html/template"
 22	golog "log"
 23	"log/syslog"
 24	notrand "math/rand"
 25	"os"
 26	"strconv"
 27	"strings"
 28	"time"
 29
 30	"humungus.tedunangst.com/r/webs/httpsig"
 31	"humungus.tedunangst.com/r/webs/log"
 32)
 33
 34var softwareVersion = "develop"
 35
 36func init() {
 37	notrand.Seed(time.Now().Unix())
 38}
 39
 40type WhatAbout struct {
 41	ID      int64
 42	Name    string
 43	Display string
 44	About   string
 45	HTAbout template.HTML
 46	Onts    []string
 47	Key     string
 48	URL     string
 49	Options UserOptions
 50	SecKey  httpsig.PrivateKey
 51}
 52
 53type UserOptions struct {
 54	SkinnyCSS    bool   `json:",omitempty"`
 55	OmitImages   bool   `json:",omitempty"`
 56	MentionAll   bool   `json:",omitempty"`
 57	InlineQuotes bool   `json:",omitempty"`
 58	Avatar       string `json:",omitempty"`
 59	Banner       string `json:",omitempty"`
 60	MapLink      string `json:",omitempty"`
 61	Reaction     string `json:",omitempty"`
 62	MeCount      int64
 63	ChatCount    int64
 64}
 65
 66type KeyInfo struct {
 67	keyname string
 68	seckey  httpsig.PrivateKey
 69}
 70
 71const serverUID int64 = -2
 72const readyLuserOne int64 = 1
 73
 74type Honk struct {
 75	ID       int64
 76	UserID   int64
 77	Username string
 78	What     string
 79	Honker   string
 80	Handle   string
 81	Handles  string
 82	Oonker   string
 83	Oondle   string
 84	XID      string
 85	RID      string
 86	Date     time.Time
 87	URL      string
 88	Noise    string
 89	Precis   string
 90	Format   string
 91	Convoy   string
 92	Audience []string
 93	Public   bool
 94	Whofore  int64
 95	Replies  []*Honk
 96	Flags    int64
 97	HTPrecis template.HTML
 98	HTML     template.HTML
 99	Style    string
100	Open     string
101	Donks    []*Donk
102	Onts     []string
103	Place    *Place
104	Time     *Time
105	Mentions []Mention
106	Badonks  []Badonk
107}
108
109type Badonk struct {
110	Who  string
111	What string
112}
113
114type Chonk struct {
115	ID     int64
116	UserID int64
117	XID    string
118	Who    string
119	Target string
120	Date   time.Time
121	Noise  string
122	Format string
123	Donks  []*Donk
124	Handle string
125	HTML   template.HTML
126}
127
128type Chatter struct {
129	Target string
130	Chonks []*Chonk
131}
132
133type Mention struct {
134	Who   string
135	Where string
136}
137
138func (mention *Mention) IsPresent(noise string) bool {
139	nick := strings.TrimLeft(mention.Who, "@")
140	idx := strings.IndexByte(nick, '@')
141	if idx != -1 {
142		nick = nick[:idx]
143	}
144	return strings.Contains(noise, ">@"+nick) || strings.Contains(noise, "@<span>"+nick)
145}
146
147func OntIsPresent(ont, noise string) bool {
148	ont = strings.ToLower(ont[1:])
149	idx := strings.IndexByte(noise, '#')
150	for idx >= 0 {
151		if strings.HasPrefix(noise[idx:], "#<span>") {
152			idx += 6
153		}
154		idx += 1
155		if idx+len(ont) >= len(noise) {
156			return false
157		}
158		test := noise[idx : idx+len(ont)]
159		test = strings.ToLower(test)
160		if test == ont {
161			return true
162		}
163		newidx := strings.IndexByte(noise[idx:], '#')
164		if newidx == -1 {
165			return false
166		}
167		idx += newidx
168	}
169	return false
170}
171
172type OldRevision struct {
173	Precis string
174	Noise  string
175}
176
177const (
178	flagIsAcked    = 1
179	flagIsBonked   = 2
180	flagIsSaved    = 4
181	flagIsUntagged = 8
182	flagIsReacted  = 16
183)
184
185func (honk *Honk) IsAcked() bool {
186	return honk.Flags&flagIsAcked != 0
187}
188
189func (honk *Honk) IsBonked() bool {
190	return honk.Flags&flagIsBonked != 0
191}
192
193func (honk *Honk) IsSaved() bool {
194	return honk.Flags&flagIsSaved != 0
195}
196
197func (honk *Honk) IsUntagged() bool {
198	return honk.Flags&flagIsUntagged != 0
199}
200
201func (honk *Honk) IsReacted() bool {
202	return honk.Flags&flagIsReacted != 0
203}
204
205type Donk struct {
206	FileID   int64
207	XID      string
208	Name     string
209	Desc     string
210	URL      string
211	Media    string
212	Local    bool
213	External bool
214}
215
216type Place struct {
217	Name      string
218	Latitude  float64
219	Longitude float64
220	Url       string
221}
222
223type Duration int64
224
225func (d Duration) String() string {
226	s := time.Duration(d).String()
227	if strings.HasSuffix(s, "m0s") {
228		s = s[:len(s)-2]
229	}
230	if strings.HasSuffix(s, "h0m") {
231		s = s[:len(s)-2]
232	}
233	return s
234}
235
236func parseDuration(s string) time.Duration {
237	didx := strings.IndexByte(s, 'd')
238	if didx != -1 {
239		days, _ := strconv.ParseInt(s[:didx], 10, 0)
240		dur, _ := time.ParseDuration(s[didx:])
241		return dur + 24*time.Hour*time.Duration(days)
242	}
243	dur, _ := time.ParseDuration(s)
244	return dur
245}
246
247type Time struct {
248	StartTime time.Time
249	EndTime   time.Time
250	Duration  Duration
251}
252
253type Honker struct {
254	ID     int64
255	UserID int64
256	Name   string
257	XID    string
258	Handle string
259	Flavor string
260	Combos []string
261	Meta   HonkerMeta
262}
263
264type HonkerMeta struct {
265	Notes string
266}
267
268type SomeThing struct {
269	What  int
270	XID   string
271	Owner string
272	Name  string
273}
274
275const (
276	SomeNothing int = iota
277	SomeActor
278	SomeCollection
279)
280
281var serverName string
282var serverPrefix string
283var masqName string
284var dataDir = "."
285var viewDir = "."
286var iconName = "icon.png"
287var serverMsg template.HTML
288var aboutMsg template.HTML
289var loginMsg template.HTML
290
291func ElaborateUnitTests() {
292}
293
294func unplugserver(hostname string) {
295	db := opendatabase()
296	xid := fmt.Sprintf("%%https://%s/%%", hostname)
297	db.Exec("delete from honkers where xid like ? and flavor = 'dub'", xid)
298	db.Exec("delete from doovers where rcpt like ?", xid)
299}
300
301func reexecArgs(cmd string) []string {
302	args := []string{"-datadir", dataDir}
303	args = append(args, log.Args()...)
304	args = append(args, cmd)
305	return args
306}
307
308var elog, ilog, dlog *golog.Logger
309
310func main() {
311	flag.StringVar(&dataDir, "datadir", dataDir, "data directory")
312	flag.StringVar(&viewDir, "viewdir", viewDir, "view directory")
313	flag.Parse()
314
315	log.Init(log.Options{Progname: "honk", Facility: syslog.LOG_UUCP})
316	elog = log.E
317	ilog = log.I
318	dlog = log.D
319
320	args := flag.Args()
321	cmd := "run"
322	if len(args) > 0 {
323		cmd = args[0]
324	}
325	switch cmd {
326	case "init":
327		initdb()
328	case "upgrade":
329		upgradedb()
330	case "version":
331		fmt.Println(softwareVersion)
332		os.Exit(0)
333	}
334	db := opendatabase()
335	dbversion := 0
336	getconfig("dbversion", &dbversion)
337	if dbversion != myVersion {
338		elog.Fatal("incorrect database version. run upgrade.")
339	}
340	getconfig("servermsg", &serverMsg)
341	getconfig("aboutmsg", &aboutMsg)
342	getconfig("loginmsg", &loginMsg)
343	getconfig("servername", &serverName)
344	getconfig("masqname", &masqName)
345	if masqName == "" {
346		masqName = serverName
347	}
348	serverPrefix = fmt.Sprintf("https://%s/", serverName)
349	getconfig("usersep", &userSep)
350	getconfig("honksep", &honkSep)
351	getconfig("devel", &develMode)
352	getconfig("fasttimeout", &fastTimeout)
353	getconfig("slowtimeout", &slowTimeout)
354	getconfig("signgets", &signGets)
355	prepareStatements(db)
356	switch cmd {
357	case "admin":
358		adminscreen()
359	case "import":
360		if len(args) != 4 {
361			elog.Fatal("import username mastodon|twitter srcdir")
362		}
363		importMain(args[1], args[2], args[3])
364	case "devel":
365		if len(args) != 2 {
366			elog.Fatal("need an argument: devel (on|off)")
367		}
368		switch args[1] {
369		case "on":
370			setconfig("devel", 1)
371		case "off":
372			setconfig("devel", 0)
373		default:
374			elog.Fatal("argument must be on or off")
375		}
376	case "setconfig":
377		if len(args) != 3 {
378			elog.Fatal("need an argument: setconfig key val")
379		}
380		var val interface{}
381		var err error
382		if val, err = strconv.Atoi(args[2]); err != nil {
383			val = args[2]
384		}
385		setconfig(args[1], val)
386	case "adduser":
387		adduser()
388	case "deluser":
389		if len(args) < 2 {
390			fmt.Printf("usage: honk deluser username\n")
391			return
392		}
393		deluser(args[1])
394	case "chpass":
395		if len(args) < 2 {
396			fmt.Printf("usage: honk chpass username\n")
397			return
398		}
399		chpass(args[1])
400	case "follow":
401		if len(args) < 3 {
402			fmt.Printf("usage: honk follow username url\n")
403			return
404		}
405		user, err := butwhatabout(args[1])
406		if err != nil {
407			fmt.Printf("user not found\n")
408			return
409		}
410		var meta HonkerMeta
411		mj, _ := jsonify(&meta)
412		honkerid, err := savehonker(user, args[2], "", "presub", "", mj)
413		if err != nil {
414			fmt.Printf("had some trouble with that: %s\n", err)
415			return
416		}
417		followyou(user, honkerid, true)
418	case "unfollow":
419		if len(args) < 3 {
420			fmt.Printf("usage: honk unfollow username url\n")
421			return
422		}
423		user, err := butwhatabout(args[1])
424		if err != nil {
425			fmt.Printf("user not found\n")
426			return
427		}
428		row := db.QueryRow("select honkerid from honkers where xid = ? and userid = ? and flavor in ('sub')", args[2], user.ID)
429		var honkerid int64
430		err = row.Scan(&honkerid)
431		if err != nil {
432			fmt.Printf("sorry couldn't find them\n")
433			return
434		}
435		unfollowyou(user, honkerid, true)
436	case "sendmsg":
437		if len(args) < 4 {
438			fmt.Printf("usage: honk send username filename rcpt\n")
439			return
440		}
441		user, err := butwhatabout(args[1])
442		if err != nil {
443			fmt.Printf("user not found\n")
444			return
445		}
446		data, err := os.ReadFile(args[2])
447		if err != nil {
448			fmt.Printf("can't read file\n")
449			return
450		}
451		deliverate(user.ID, args[3], data)
452	case "cleanup":
453		arg := "30"
454		if len(args) > 1 {
455			arg = args[1]
456		}
457		cleanupdb(arg)
458	case "unplug":
459		if len(args) < 2 {
460			fmt.Printf("usage: honk unplug servername\n")
461			return
462		}
463		name := args[1]
464		unplugserver(name)
465	case "backup":
466		if len(args) < 2 {
467			fmt.Printf("usage: honk backup dirname\n")
468			return
469		}
470		name := args[1]
471		svalbard(name)
472	case "ping":
473		if len(args) < 3 {
474			fmt.Printf("usage: honk ping (from username) (to username or url)\n")
475			return
476		}
477		name := args[1]
478		targ := args[2]
479		user, err := butwhatabout(name)
480		if err != nil {
481			elog.Printf("unknown user")
482			return
483		}
484		ping(user, targ)
485	case "run":
486		serve()
487	case "backend":
488		backendServer()
489	case "test":
490		ElaborateUnitTests()
491	default:
492		elog.Fatal("unknown command")
493	}
494}