2019-04-09 13:59:33 +02:00
//
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"database/sql"
"fmt"
"html"
"html/template"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"log"
"net/http"
"os"
"regexp"
"sort"
"strings"
"sync"
"time"
"github.com/gorilla/mux"
)
type UserInfo struct {
UserID int64
Username string
}
type WhatAbout struct {
ID int64
Name string
Display string
About string
Key string
URL string
}
var serverName string
var iconName = "icon.png"
var readviews * Template
func ziggy ( user * WhatAbout ) ( keyname string , key * rsa . PrivateKey ) {
db := opendatabase ( )
row := db . QueryRow ( "select seckey from users where userid = ?" , user . ID )
var data string
row . Scan ( & data )
var err error
key , _ , err = pez ( data )
if err != nil {
log . Printf ( "error loading %s seckey: %s" , user . Name , err )
}
keyname = user . URL + "#key"
return
}
func zaggy ( keyname string ) ( key * rsa . PublicKey ) {
db := opendatabase ( )
2019-04-11 00:42:22 +02:00
row := db . QueryRow ( "select pubkey from honkers where flavor = 'key' and xid = ?" , keyname )
2019-04-09 13:59:33 +02:00
var data string
err := row . Scan ( & data )
savekey := false
if err != nil {
savekey = true
j , err := GetJunk ( keyname )
if err != nil {
log . Printf ( "error getting %s pubkey: %s" , keyname , err )
return
}
var ok bool
data , ok = jsonfindstring ( j , [ ] string { "publicKey" , "publicKeyPem" } )
if ! ok {
log . Printf ( "error getting %s pubkey" , keyname )
return
}
_ , ok = jsonfindstring ( j , [ ] string { "publicKey" , "owner" } )
if ! ok {
log . Printf ( "error getting %s pubkey owner" , keyname )
return
}
}
_ , key , err = pez ( data )
if err != nil {
log . Printf ( "error getting %s pubkey: %s" , keyname , err )
return
}
if savekey {
db . Exec ( "insert into honkers (name, xid, flavor, pubkey) values (?, ?, ?, ?)" ,
"" , keyname , "key" , data )
}
return
}
func keymatch ( keyname string , actor string ) bool {
return strings . HasPrefix ( keyname , actor )
}
func getInfo ( r * http . Request ) map [ string ] interface { } {
templinfo := make ( map [ string ] interface { } )
templinfo [ "StyleParam" ] = getstyleparam ( )
templinfo [ "ServerName" ] = serverName
templinfo [ "IconName" ] = iconName
templinfo [ "UserInfo" ] = GetUserInfo ( r )
templinfo [ "LogoutCSRF" ] = GetCSRF ( "logout" , r )
return templinfo
}
var re_unurl = regexp . MustCompile ( "https://([^/]+).*/([^/]+)" )
func honkerhandle ( h string ) string {
m := re_unurl . FindStringSubmatch ( h )
if len ( m ) > 2 {
return fmt . Sprintf ( "%s@%s" , m [ 2 ] , m [ 1 ] )
}
return ""
}
func reverbolate ( honks [ ] * Honk ) {
for _ , h := range honks {
h . What += "ed"
if h . Honker == "" {
h . Honker = "https://" + serverName + "/u/" + h . Username
if strings . IndexByte ( h . XID , '/' ) == - 1 {
h . URL = h . Honker + "/h/" + h . XID
} else {
h . URL = h . XID
}
} else {
idx := strings . LastIndexByte ( h . Honker , '/' )
if idx != - 1 {
h . Username = honkerhandle ( h . Honker )
} else {
h . Username = h . Honker
}
if h . URL == "" {
h . URL = h . XID
}
}
h . HTML = cleanstring ( h . Noise )
}
}
func homepage ( w http . ResponseWriter , r * http . Request ) {
templinfo := getInfo ( r )
honks := gethonks ( "" )
u := GetUserInfo ( r )
if u != nil {
morehonks := gethonksforuser ( u . UserID )
honks = append ( honks , morehonks ... )
templinfo [ "HonkCSRF" ] = GetCSRF ( "honkhonk" , r )
}
sort . Slice ( honks , func ( i , j int ) bool {
return honks [ i ] . Date . After ( honks [ j ] . Date )
} )
reverbolate ( honks )
2019-04-11 01:25:32 +02:00
var modtime time . Time
if len ( honks ) > 0 {
modtime = honks [ 0 ] . Date
}
imh := r . Header . Get ( "If-Modified-Since" )
if imh != "" && ! modtime . IsZero ( ) {
ifmod , err := time . Parse ( http . TimeFormat , imh )
if err == nil && ! modtime . After ( ifmod ) {
w . WriteHeader ( http . StatusNotModified )
return
}
}
2019-04-09 13:59:33 +02:00
msg := "Things happen."
getconfig ( "servermsg" , & msg )
templinfo [ "Honks" ] = honks
templinfo [ "ShowRSS" ] = true
templinfo [ "ServerMessage" ] = msg
2019-04-11 01:25:32 +02:00
w . Header ( ) . Set ( "Cache-Control" , "max-age=0" )
w . Header ( ) . Set ( "Last-Modified" , modtime . Format ( http . TimeFormat ) )
2019-04-09 13:59:33 +02:00
err := readviews . ExecuteTemplate ( w , "homepage.html" , templinfo )
if err != nil {
log . Print ( err )
}
}
func showrss ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
honks := gethonks ( name )
sort . Slice ( honks , func ( i , j int ) bool {
return honks [ i ] . Date . After ( honks [ j ] . Date )
} )
reverbolate ( honks )
home := fmt . Sprintf ( "https://%s/" , serverName )
base := home
if name != "" {
home += "u/" + name
name += " "
}
feed := RssFeed {
Title : name + "honk" ,
Link : home ,
Description : name + "honk rss" ,
FeedImage : & RssFeedImage {
URL : base + "icon.png" ,
Title : name + "honk rss" ,
Link : home ,
} ,
}
var modtime time . Time
past := time . Now ( ) . UTC ( ) . Add ( - 3 * 24 * time . Hour )
for _ , honk := range honks {
if honk . Date . Before ( past ) {
break
}
if honk . URL [ 0 ] == '/' {
honk . URL = "https://" + serverName + honk . URL
}
feed . Items = append ( feed . Items , & RssItem {
Title : fmt . Sprintf ( "%s %s %s" , honk . Username , honk . What , honk . XID ) ,
Description : RssCData { string ( honk . HTML ) } ,
Link : honk . URL ,
PubDate : honk . Date . Format ( time . RFC1123 ) ,
} )
if honk . Date . After ( modtime ) {
modtime = honk . Date
}
}
w . Header ( ) . Set ( "Cache-Control" , "max-age=300" )
w . Header ( ) . Set ( "Last-Modified" , modtime . Format ( http . TimeFormat ) )
err := feed . Write ( w )
if err != nil {
log . Printf ( "error writing rss: %s" , err )
}
}
func butwhatabout ( name string ) ( * WhatAbout , error ) {
row := stmtWhatAbout . QueryRow ( name )
var user WhatAbout
err := row . Scan ( & user . ID , & user . Name , & user . Display , & user . About , & user . Key )
user . URL = fmt . Sprintf ( "https://%s/u/%s" , serverName , user . Name )
return & user , err
}
func crappola ( j map [ string ] interface { } ) bool {
t , _ := jsongetstring ( j , "type" )
a , _ := jsongetstring ( j , "actor" )
o , _ := jsongetstring ( j , "object" )
if t == "Delete" && a == o {
log . Printf ( "crappola from %s" , a )
return true
}
return false
}
func inbox ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
var buf bytes . Buffer
io . Copy ( & buf , r . Body )
payload := buf . Bytes ( )
j , err := ReadJunk ( bytes . NewReader ( payload ) )
if err != nil {
log . Printf ( "bad payload: %s" , err )
io . WriteString ( os . Stdout , "bad payload\n" )
os . Stdout . Write ( payload )
io . WriteString ( os . Stdout , "\n" )
return
}
if crappola ( j ) {
return
}
keyname , err := zag ( r , payload )
if err != nil {
log . Printf ( "inbox message failed signature: %s" , err )
fd , _ := os . OpenFile ( "savedinbox.json" , os . O_CREATE | os . O_WRONLY | os . O_APPEND , 0666 )
io . WriteString ( fd , "bad signature:\n" )
WriteJunk ( fd , j )
io . WriteString ( fd , "\n" )
fd . Close ( )
return
}
fd , _ := os . OpenFile ( "savedinbox.json" , os . O_CREATE | os . O_WRONLY | os . O_APPEND , 0666 )
WriteJunk ( fd , j )
io . WriteString ( fd , "\n" )
fd . Close ( )
who , _ := jsongetstring ( j , "actor" )
if ! keymatch ( keyname , who ) {
log . Printf ( "keyname actor mismatch: %s <> %s" , keyname , who )
return
}
what , _ := jsongetstring ( j , "type" )
switch what {
case "Follow" :
log . Printf ( "updating honker follow: %s" , who )
rubadubdub ( user , j )
case "Accept" :
db := opendatabase ( )
log . Printf ( "updating honker accept: %s" , who )
db . Exec ( "update honkers set flavor = 'sub' where xid = ? and flavor = 'presub'" , who )
case "Undo" :
obj , ok := jsongetmap ( j , "object" )
if ! ok {
log . Printf ( "unknown undo no object" )
} else {
what , _ := jsongetstring ( obj , "type" )
if what != "Follow" {
log . Printf ( "unknown undo: %s" , what )
} else {
log . Printf ( "updating honker undo: %s" , who )
db := opendatabase ( )
db . Exec ( "update honkers set flavor = 'undub' where xid = ? and flavor = 'dub'" , who )
}
}
default :
xonk := xonkxonk ( j )
if xonk != nil && needxonk ( user . ID , xonk ) {
xonk . UserID = user . ID
savexonk ( xonk )
}
}
}
func outbox ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
honks := gethonks ( name )
var jonks [ ] map [ string ] interface { }
for _ , h := range honks {
j , _ := jonkjonk ( user , h )
jonks = append ( jonks , j )
}
j := NewJunk ( )
j [ "@context" ] = itiswhatitis
j [ "id" ] = user . URL + "/outbox"
j [ "type" ] = "OrderedCollection"
j [ "totalItems" ] = len ( jonks )
j [ "orderedItems" ] = jonks
w . Header ( ) . Set ( "Content-Type" , theonetruename )
WriteJunk ( w , j )
}
func viewuser ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
if friendorfoe ( r . Header . Get ( "Accept" ) ) {
j := asjonker ( user )
w . Header ( ) . Set ( "Content-Type" , theonetruename )
WriteJunk ( w , j )
return
}
honks := gethonks ( name )
u := GetUserInfo ( r )
honkpage ( w , r , u , user , honks )
}
func viewhonker ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
u := GetUserInfo ( r )
honks := gethonksbyhonker ( u . UserID , name )
honkpage ( w , r , nil , nil , honks )
}
func fingerlicker ( w http . ResponseWriter , r * http . Request ) {
orig := r . FormValue ( "resource" )
log . Printf ( "finger lick: %s" , orig )
if strings . HasPrefix ( orig , "acct:" ) {
orig = orig [ 5 : ]
}
name := orig
idx := strings . LastIndexByte ( name , '/' )
if idx != - 1 {
name = name [ idx + 1 : ]
if "https://" + serverName + "/u/" + name != orig {
log . Printf ( "foreign request rejected" )
name = ""
}
} else {
idx = strings . IndexByte ( name , '@' )
if idx != - 1 {
name = name [ : idx ]
if name + "@" + serverName != orig {
log . Printf ( "foreign request rejected" )
name = ""
}
}
}
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
j := NewJunk ( )
j [ "subject" ] = fmt . Sprintf ( "acct:%s@%s" , user . Name , serverName )
j [ "aliases" ] = [ ] string { user . URL }
var links [ ] map [ string ] interface { }
l := NewJunk ( )
l [ "rel" ] = "self"
l [ "type" ] = ` application/activity+json `
l [ "href" ] = user . URL
links = append ( links , l )
j [ "links" ] = links
w . Header ( ) . Set ( "Content-Type" , "application/jrd+json" )
WriteJunk ( w , j )
}
func viewhonk ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
xid := mux . Vars ( r ) [ "xid" ]
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
h := getxonk ( name , xid )
if h == nil {
http . NotFound ( w , r )
return
}
if friendorfoe ( r . Header . Get ( "Accept" ) ) {
_ , j := jonkjonk ( user , h )
j [ "@context" ] = itiswhatitis
w . Header ( ) . Set ( "Content-Type" , theonetruename )
WriteJunk ( w , j )
return
}
honkpage ( w , r , nil , nil , [ ] * Honk { h } )
}
func honkpage ( w http . ResponseWriter , r * http . Request , u * UserInfo , user * WhatAbout , honks [ ] * Honk ) {
reverbolate ( honks )
templinfo := getInfo ( r )
if u != nil && u . Username == user . Name {
templinfo [ "UserCSRF" ] = GetCSRF ( "saveuser" , r )
templinfo [ "HonkCSRF" ] = GetCSRF ( "honkhonk" , r )
}
if user != nil {
templinfo [ "Name" ] = user . Name
whatabout := user . About
templinfo [ "RawWhatAbout" ] = whatabout
whatabout = obfusbreak ( whatabout )
templinfo [ "WhatAbout" ] = cleanstring ( whatabout )
}
templinfo [ "Honks" ] = honks
err := readviews . ExecuteTemplate ( w , "honkpage.html" , templinfo )
if err != nil {
log . Print ( err )
}
}
func saveuser ( w http . ResponseWriter , r * http . Request ) {
whatabout := r . FormValue ( "whatabout" )
u := GetUserInfo ( r )
db := opendatabase ( )
_ , err := db . Exec ( "update users set about = ? where username = ?" , whatabout , u . Username )
if err != nil {
log . Printf ( "error bouting what: %s" , err )
}
http . Redirect ( w , r , "/u/" + u . Username , http . StatusSeeOther )
}
type Donk struct {
FileID int64
XID string
Name string
URL string
Media string
Content [ ] byte
}
type Honk struct {
ID int64
UserID int64
Username string
What string
Honker string
XID string
RID string
Date time . Time
URL string
Noise string
Audience [ ] string
HTML template . HTML
Donks [ ] * Donk
}
type Honker struct {
ID int64
UserID int64
Name string
XID string
Flavor string
}
func gethonkers ( userid int64 ) [ ] * Honker {
rows , err := stmtHonkers . Query ( userid )
if err != nil {
log . Printf ( "error querying honkers: %s" , err )
return nil
}
defer rows . Close ( )
var honkers [ ] * Honker
for rows . Next ( ) {
var f Honker
err = rows . Scan ( & f . ID , & f . UserID , & f . Name , & f . XID , & f . Flavor )
if err != nil {
log . Printf ( "error scanning honker: %s" , err )
return nil
}
honkers = append ( honkers , & f )
}
return honkers
}
func getdubs ( userid int64 ) [ ] * Honker {
rows , err := stmtDubbers . Query ( userid )
if err != nil {
log . Printf ( "error querying dubs: %s" , err )
return nil
}
defer rows . Close ( )
var honkers [ ] * Honker
for rows . Next ( ) {
var f Honker
err = rows . Scan ( & f . ID , & f . UserID , & f . Name , & f . XID , & f . Flavor )
if err != nil {
log . Printf ( "error scanning honker: %s" , err )
return nil
}
honkers = append ( honkers , & f )
}
return honkers
}
func gethonk ( honkid int64 ) * Honk {
var h Honk
var dt , aud string
row := stmtOneHonk . QueryRow ( honkid )
err := row . Scan ( & h . ID , & h . UserID , & h . Username , & h . What , & h . Honker , & h . XID , & h . RID ,
& dt , & h . URL , & aud , & h . Noise )
if err != nil {
log . Printf ( "error scanning honk: %s" , err )
return nil
}
h . Date , _ = time . Parse ( dbtimeformat , dt )
h . Audience = strings . Split ( aud , " " )
return & h
}
func getxonk ( name , xid string ) * Honk {
var h Honk
var dt , aud string
row := stmtOneXonk . QueryRow ( xid )
err := row . Scan ( & h . ID , & h . UserID , & h . Username , & h . What , & h . Honker , & h . XID , & h . RID ,
& dt , & h . URL , & aud , & h . Noise )
if err != nil {
log . Printf ( "error scanning xonk: %s" , err )
return nil
}
if name != "" && h . Username != name {
log . Printf ( "user xonk mismatch" )
return nil
}
h . Date , _ = time . Parse ( dbtimeformat , dt )
h . Audience = strings . Split ( aud , " " )
donksforhonks ( [ ] * Honk { & h } )
return & h
}
func gethonks ( username string ) [ ] * Honk {
return getsomehonks ( username , 0 , "" )
}
func gethonksforuser ( userid int64 ) [ ] * Honk {
return getsomehonks ( "" , userid , "" )
}
func gethonksbyhonker ( userid int64 , honker string ) [ ] * Honk {
return getsomehonks ( "" , userid , honker )
}
func getsomehonks ( username string , userid int64 , honkername string ) [ ] * Honk {
var rows * sql . Rows
var err error
if username != "" {
rows , err = stmtUserHonks . Query ( username )
} else if honkername != "" {
rows , err = stmtHonksByHonker . Query ( userid , honkername )
} else if userid > 0 {
rows , err = stmtHonksForUser . Query ( userid )
} else {
rows , err = stmtHonks . Query ( )
}
if err != nil {
log . Printf ( "error querying honks: %s" , err )
return nil
}
defer rows . Close ( )
var honks [ ] * Honk
for rows . Next ( ) {
var h Honk
var dt , aud string
err = rows . Scan ( & h . ID , & h . UserID , & h . Username , & h . What , & h . Honker , & h . XID , & h . RID ,
& dt , & h . URL , & aud , & h . Noise )
if err != nil {
log . Printf ( "error scanning honks: %s" , err )
return nil
}
h . Date , _ = time . Parse ( dbtimeformat , dt )
h . Audience = strings . Split ( aud , " " )
honks = append ( honks , & h )
}
rows . Close ( )
donksforhonks ( honks )
return honks
}
func donksforhonks ( honks [ ] * Honk ) {
db := opendatabase ( )
var ids [ ] string
for _ , h := range honks {
ids = append ( ids , fmt . Sprintf ( "%d" , h . ID ) )
}
q := fmt . Sprintf ( "select honkid, donks.fileid, xid, name, url, media from donks join files on donks.fileid = files.fileid where honkid in (%s)" , strings . Join ( ids , "," ) )
rows , err := db . Query ( q )
if err != nil {
log . Printf ( "error querying donks: %s" , err )
return
}
defer rows . Close ( )
for rows . Next ( ) {
var hid int64
var d Donk
err = rows . Scan ( & hid , & d . FileID , & d . XID , & d . Name , & d . URL , & d . Media )
if err != nil {
log . Printf ( "error scanning donk: %s" , err )
continue
}
for _ , h := range honks {
if h . ID == hid {
h . Donks = append ( h . Donks , & d )
}
}
}
}
func xfiltrate ( ) string {
letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
db := opendatabase ( )
for {
var x int64
var b [ 16 ] byte
rand . Read ( b [ : ] )
for i , c := range b {
b [ i ] = letters [ c & 63 ]
}
s := string ( b [ : ] )
r := db . QueryRow ( "select honkid from honks where xid = ?" , s )
err := r . Scan ( & x )
if err == nil {
continue
}
if err != sql . ErrNoRows {
log . Printf ( "err picking xid: %s" , err )
return ""
}
r = db . QueryRow ( "select fileid from files where name = ?" , s )
err = r . Scan ( & x )
if err == nil {
continue
}
if err != sql . ErrNoRows {
log . Printf ( "err picking xid: %s" , err )
return ""
}
return s
}
}
type Mention struct {
who string
where string
}
var re_mentions = regexp . MustCompile ( ` @[[:alnum:]]+@[[:alnum:].]+ ` )
func grapevine ( s string ) [ ] string {
m := re_mentions . FindAllString ( s , - 1 )
var mentions [ ] string
for i := range m {
where := gofish ( m [ i ] )
if where != "" {
mentions = append ( mentions , where )
}
}
return mentions
}
func bunchofgrapes ( s string ) [ ] Mention {
m := re_mentions . FindAllString ( s , - 1 )
var mentions [ ] Mention
for i := range m {
where := gofish ( m [ i ] )
if where != "" {
mentions = append ( mentions , Mention { who : m [ i ] , where : where } )
}
}
return mentions
}
var re_link = regexp . MustCompile ( ` https?://[^\s"]+[\w/)] ` )
func obfusbreak ( s string ) string {
s = strings . TrimSpace ( s )
s = strings . Replace ( s , "\r" , "" , - 1 )
s = html . EscapeString ( s )
linkfn := func ( url string ) string {
addparen := false
adddot := false
if strings . HasSuffix ( url , ")" ) && strings . IndexByte ( url , '(' ) == - 1 {
url = url [ : len ( url ) - 1 ]
addparen = true
}
if strings . HasSuffix ( url , "." ) {
url = url [ : len ( url ) - 1 ]
adddot = true
}
url = fmt . Sprintf ( ` <a href="%s">%s</a> ` , url , url )
if adddot {
url += "."
}
if addparen {
url += ")"
}
return url
}
s = re_link . ReplaceAllStringFunc ( s , linkfn )
s = strings . Replace ( s , "\n" , "<br>" , - 1 )
s = re_mentions . ReplaceAllStringFunc ( s , func ( m string ) string {
return fmt . Sprintf ( ` <a href="%s">%s</a> ` , html . EscapeString ( gofish ( m ) ) ,
html . EscapeString ( m ) )
} )
return s
}
func prepend ( s string , x [ ] string ) [ ] string {
return append ( [ ] string { s } , x ... )
}
func savebonk ( w http . ResponseWriter , r * http . Request ) {
xid := r . FormValue ( "xid" )
log . Printf ( "bonking %s" , xid )
xonk := getxonk ( "" , xid )
if xonk == nil {
return
}
if xonk . Honker == "" {
xonk . XID = fmt . Sprintf ( "https://%s/u/%s/h/%s" , serverName , xonk . Username , xonk . XID )
}
userinfo := GetUserInfo ( r )
dt := time . Now ( ) . UTC ( )
bonk := Honk {
UserID : userinfo . UserID ,
Username : userinfo . Username ,
Honker : xonk . Honker ,
What : "bonk" ,
XID : xonk . XID ,
Date : dt ,
Noise : xonk . Noise ,
Donks : xonk . Donks ,
Audience : oneofakind ( prepend ( thewholeworld , xonk . Audience ) ) ,
}
aud := strings . Join ( bonk . Audience , " " )
res , err := stmtSaveHonk . Exec ( userinfo . UserID , "bonk" , "" , xid , "" ,
dt . Format ( dbtimeformat ) , "" , aud , bonk . Noise )
if err != nil {
log . Printf ( "error saving bonk: %s" , err )
return
}
bonk . ID , _ = res . LastInsertId ( )
for _ , d := range bonk . Donks {
_ , err = stmtSaveDonk . Exec ( bonk . ID , d . FileID )
if err != nil {
log . Printf ( "err saving donk: %s" , err )
return
}
}
user , _ := butwhatabout ( userinfo . Username )
go honkworldwide ( user , & bonk )
}
func savehonk ( w http . ResponseWriter , r * http . Request ) {
rid := r . FormValue ( "rid" )
noise := r . FormValue ( "noise" )
userinfo := GetUserInfo ( r )
dt := time . Now ( ) . UTC ( )
xid := xfiltrate ( )
if xid == "" {
return
}
what := "honk"
if rid != "" {
what = "tonk"
}
honk := Honk {
UserID : userinfo . UserID ,
Username : userinfo . Username ,
What : "honk" ,
XID : xid ,
RID : rid ,
Date : dt ,
}
if noise [ 0 ] == '@' {
honk . Audience = append ( grapevine ( noise ) , thewholeworld )
} else {
honk . Audience = append ( [ ] string { thewholeworld } , grapevine ( noise ) ... )
}
if rid != "" {
xonk := getxonk ( "" , rid )
honk . Audience = append ( honk . Audience , xonk . Audience ... )
}
honk . Audience = oneofakind ( honk . Audience )
noise = obfusbreak ( noise )
honk . Noise = noise
file , _ , err := r . FormFile ( "donk" )
if err == nil {
var buf bytes . Buffer
io . Copy ( & buf , file )
file . Close ( )
data := buf . Bytes ( )
img , format , err := image . Decode ( & buf )
if err != nil {
log . Printf ( "bad image: %s" , err )
return
}
data , format , err = vacuumwrap ( img , format )
if err != nil {
log . Printf ( "can't vacuum image: %s" , err )
return
}
name := xfiltrate ( )
media := "image/" + format
if format == "jpeg" {
format = "jpg"
}
name = name + "." + format
url := fmt . Sprintf ( "https://%s/d/%s" , serverName , name )
res , err := stmtSaveFile . Exec ( name , name , url , media , data )
if err != nil {
log . Printf ( "unable to save image: %s" , err )
return
}
var d Donk
d . FileID , _ = res . LastInsertId ( )
d . XID = name
d . Name = name
d . Media = media
d . URL = url
honk . Donks = append ( honk . Donks , & d )
}
aud := strings . Join ( honk . Audience , " " )
res , err := stmtSaveHonk . Exec ( userinfo . UserID , what , "" , xid , rid ,
dt . Format ( dbtimeformat ) , "" , aud , noise )
if err != nil {
log . Printf ( "error saving honk: %s" , err )
return
}
honk . ID , _ = res . LastInsertId ( )
for _ , d := range honk . Donks {
_ , err = stmtSaveDonk . Exec ( honk . ID , d . FileID )
if err != nil {
log . Printf ( "err saving donk: %s" , err )
return
}
}
user , _ := butwhatabout ( userinfo . Username )
go honkworldwide ( user , & honk )
http . Redirect ( w , r , "/" , http . StatusSeeOther )
}
func showhonkers ( w http . ResponseWriter , r * http . Request ) {
userinfo := GetUserInfo ( r )
templinfo := getInfo ( r )
templinfo [ "Honkers" ] = gethonkers ( userinfo . UserID )
templinfo [ "HonkerCSRF" ] = GetCSRF ( "savehonker" , r )
err := readviews . ExecuteTemplate ( w , "honkers.html" , templinfo )
if err != nil {
log . Print ( err )
}
}
var handfull = make ( map [ string ] string )
var handlock sync . Mutex
func gofish ( name string ) string {
if name [ 0 ] == '@' {
name = name [ 1 : ]
}
m := strings . Split ( name , "@" )
if len ( m ) != 2 {
log . Printf ( "bad far name: %s" , name )
return ""
}
handlock . Lock ( )
defer handlock . Unlock ( )
ref , ok := handfull [ name ]
if ok {
return ref
}
j , err := GetJunk ( fmt . Sprintf ( "https://%s/.well-known/webfinger?resource=acct:%s" , m [ 1 ] , name ) )
if err != nil {
log . Printf ( "failed to get far name: %s" , err )
handfull [ name ] = ""
return ""
}
links , _ := jsongetarray ( j , "links" )
for _ , l := range links {
href , _ := jsongetstring ( l , "href" )
rel , _ := jsongetstring ( l , "rel" )
t , _ := jsongetstring ( l , "type" )
if rel == "self" && friendorfoe ( t ) {
handfull [ name ] = href
return href
}
}
handfull [ name ] = ""
return ""
}
func savehonker ( w http . ResponseWriter , r * http . Request ) {
name := r . FormValue ( "name" )
url := r . FormValue ( "url" )
peep := r . FormValue ( "peep" )
flavor := "presub"
if peep == "peep" {
flavor = "peep"
}
if url == "" {
return
}
if url [ 0 ] == '@' {
url = gofish ( url )
}
if url == "" {
return
}
u := GetUserInfo ( r )
db := opendatabase ( )
_ , err := db . Exec ( "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)" ,
u . UserID , name , url , flavor )
if err != nil {
log . Print ( err )
}
if flavor == "presub" {
user , _ := butwhatabout ( u . Username )
go subsub ( user , url )
}
http . Redirect ( w , r , "/honkers" , http . StatusSeeOther )
}
func avatate ( w http . ResponseWriter , r * http . Request ) {
n := r . FormValue ( "a" )
a := avatar ( n )
w . Header ( ) . Set ( "Cache-Control" , "max-age=76000" )
w . Write ( a )
}
func servecss ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Cache-Control" , "max-age=7776000" )
http . ServeFile ( w , r , "views" + r . URL . Path )
}
func servehtml ( w http . ResponseWriter , r * http . Request ) {
templinfo := getInfo ( r )
err := readviews . ExecuteTemplate ( w , r . URL . Path [ 1 : ] + ".html" , templinfo )
if err != nil {
log . Print ( err )
}
}
func servefile ( w http . ResponseWriter , r * http . Request ) {
xid := mux . Vars ( r ) [ "xid" ]
row := stmtFileData . QueryRow ( xid )
var data [ ] byte
err := row . Scan ( & data )
if err != nil {
log . Printf ( "error loading file: %s" , err )
http . NotFound ( w , r )
return
}
w . Header ( ) . Set ( "Cache-Control" , "max-age=432000" )
w . Write ( data )
}
func serve ( ) {
db := opendatabase ( )
LoginInit ( db )
getconfig ( "servername" , & serverName )
listener , err := openListener ( )
if err != nil {
log . Fatal ( err )
}
debug := false
getconfig ( "debug" , & debug )
readviews = ParseTemplates ( debug ,
"views/homepage.html" ,
"views/honkpage.html" ,
"views/honkers.html" ,
"views/honkform.html" ,
"views/honk.html" ,
"views/login.html" ,
"views/header.html" ,
)
if ! debug {
savedstyleparam = getstyleparam ( )
}
mux := mux . NewRouter ( )
mux . Use ( LoginChecker )
posters := mux . Methods ( "POST" ) . Subrouter ( )
getters := mux . Methods ( "GET" ) . Subrouter ( )
getters . HandleFunc ( "/" , homepage )
getters . HandleFunc ( "/rss" , showrss )
getters . HandleFunc ( "/u/{name:[[:alnum:]]+}" , viewuser )
getters . HandleFunc ( "/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}" , viewhonk )
getters . HandleFunc ( "/u/{name:[[:alnum:]]+}/rss" , showrss )
posters . HandleFunc ( "/u/{name:[[:alnum:]]+}/inbox" , inbox )
getters . HandleFunc ( "/u/{name:[[:alnum:]]+}/outbox" , outbox )
getters . HandleFunc ( "/a" , avatate )
getters . HandleFunc ( "/d/{xid:[[:alnum:].]+}" , servefile )
getters . HandleFunc ( "/h/{name:[[:alnum:]]+}" , viewhonker )
getters . HandleFunc ( "/.well-known/webfinger" , fingerlicker )
getters . HandleFunc ( "/style.css" , servecss )
getters . HandleFunc ( "/login" , servehtml )
posters . HandleFunc ( "/dologin" , dologin )
getters . HandleFunc ( "/logout" , dologout )
loggedin := mux . NewRoute ( ) . Subrouter ( )
loggedin . Use ( LoginRequired )
loggedin . Handle ( "/honk" , CSRFWrap ( "honkhonk" , http . HandlerFunc ( savehonk ) ) )
loggedin . Handle ( "/bonk" , CSRFWrap ( "honkhonk" , http . HandlerFunc ( savebonk ) ) )
loggedin . Handle ( "/saveuser" , CSRFWrap ( "saveuser" , http . HandlerFunc ( saveuser ) ) )
loggedin . HandleFunc ( "/honkers" , showhonkers )
loggedin . Handle ( "/savehonker" , CSRFWrap ( "savehonker" , http . HandlerFunc ( savehonker ) ) )
err = http . Serve ( listener , mux )
if err != nil {
log . Fatal ( err )
}
}
var stmtHonkers , stmtDubbers , stmtOneHonk , stmtOneXonk , stmtHonks , stmtUserHonks * sql . Stmt
var stmtHonksForUser , stmtDeleteHonk , stmtSaveDub * sql . Stmt
var stmtHonksByHonker , stmtSaveHonk , stmtFileData , stmtWhatAbout * sql . Stmt
var stmtFindXonk , stmtSaveDonk , stmtFindFile , stmtSaveFile * sql . Stmt
func prepareStatements ( db * sql . DB ) {
var err error
stmtHonkers , err = db . Prepare ( "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'" )
if err != nil {
log . Fatal ( err )
}
stmtDubbers , err = db . Prepare ( "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'" )
if err != nil {
log . Fatal ( err )
}
stmtOneHonk , err = db . Prepare ( "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where honkid = ? limit 50" )
if err != nil {
log . Fatal ( err )
}
stmtOneXonk , err = db . Prepare ( "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where xid = ?" )
if err != nil {
log . Fatal ( err )
}
stmtHonks , err = db . Prepare ( "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where honker = '' order by honkid desc limit 50" )
if err != nil {
log . Fatal ( err )
}
stmtUserHonks , err = db . Prepare ( "select honkid, honks.userid, username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where honker = '' and username = ? order by honkid desc limit 50" )
if err != nil {
log . Fatal ( err )
}
stmtHonksForUser , err = db . Prepare ( "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where honks.userid = ? and honker <> '' and what <> 'zonk' order by honkid desc limit 150" )
if err != nil {
log . Fatal ( err )
}
stmtHonksByHonker , err = db . Prepare ( "select honkid, honks.userid, users.username, what, honker, honks.xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ? order by honkid desc limit 50" )
if err != nil {
log . Fatal ( err )
}
stmtSaveHonk , err = db . Prepare ( "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise) values (?, ?, ?, ?, ?, ?, ?, ?, ?)" )
if err != nil {
log . Fatal ( err )
}
stmtFileData , err = db . Prepare ( "select content from files where xid = ?" )
if err != nil {
log . Fatal ( err )
}
stmtFindXonk , err = db . Prepare ( "select honkid from honks where userid = ? and xid = ? and what = ?" )
if err != nil {
log . Fatal ( err )
}
stmtSaveDonk , err = db . Prepare ( "insert into donks (honkid, fileid) values (?, ?)" )
if err != nil {
log . Fatal ( err )
}
stmtDeleteHonk , err = db . Prepare ( "update honks set what = 'zonk' where xid = ? and honker = ?" )
if err != nil {
log . Fatal ( err )
}
stmtFindFile , err = db . Prepare ( "select fileid from files where url = ?" )
if err != nil {
log . Fatal ( err )
}
stmtSaveFile , err = db . Prepare ( "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)" )
if err != nil {
log . Fatal ( err )
}
stmtWhatAbout , err = db . Prepare ( "select userid, username, displayname, about, pubkey from users where username = ?" )
if err != nil {
log . Fatal ( err )
}
stmtSaveDub , err = db . Prepare ( "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)" )
if err != nil {
log . Fatal ( err )
}
}
func ElaborateUnitTests ( ) {
}
func finishusersetup ( ) error {
db := opendatabase ( )
k , err := rsa . GenerateKey ( rand . Reader , 2048 )
if err != nil {
return err
}
pubkey , err := zem ( & k . PublicKey )
if err != nil {
return err
}
seckey , err := zem ( k )
if err != nil {
return err
}
_ , err = db . Exec ( "update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1" , "what about me?" , pubkey , seckey )
if err != nil {
return err
}
return nil
}
func main ( ) {
cmd := "run"
if len ( os . Args ) > 1 {
cmd = os . Args [ 1 ]
}
if cmd != "init" {
db := opendatabase ( )
prepareStatements ( db )
}
switch cmd {
case "peep" :
peeppeep ( )
case "init" :
initdb ( )
case "run" :
serve ( )
case "test" :
ElaborateUnitTests ( )
default :
log . Fatal ( "unknown command" )
}
}